﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;

namespace GISCore.Geometry.IO
{
    public class GMLFileHandler : IFileHandler 
    {
        public int LayerID { get; set; }

        public GMLFileHandler() { }

        public GMLFileHandler(int layer)
        {
            LayerID = layer;            
        }

        private Layer _parse(int layerId, XmlTextReader myParser)
        {
            double avgLength = 0;
            double numberOfEdges = 0, totalEdgeLength = 0;
            Point boundingBoxLower = null;
            Point boundingBoxUpper = null;
            var polygons = new List<Polygon>();

            /* You need to know if current points make a polygon or hole */
            int isPoly = -1;
            Polygon currentPolygon = null;

            /* Read the XML elements */
            while (myParser.Read())
            {
                /* if this is the bounding box for the layer - Digg it */
                if (myParser.Name.Equals("gml:lowerCorner", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    String[] points = myParser.ReadString().Split();
                    boundingBoxLower = new Point(double.Parse(points[0]), double.Parse(points[1]));
                    //set to Constants
                    //Constants.setLowerBoundingBox(this.boundingBoxLower);
                }
                if (myParser.Name.Equals("gml:upperCorner", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    String[] points = myParser.ReadString().Split();
                    boundingBoxUpper = new Point(double.Parse(points[0]), double.Parse(points[1]));
                    //set to Constants
                    //Constants.setUpperBoundingBox(this.boundingBoxUpper);
                    break;
                }
            }
            /* Read the rest now */
            while (myParser.Read())
            {
                /* There are two kind of elements in the world. The substantial ones called polygons,
                 * and the hideous ones called holes. Both have pos lists but the element outside 
                 * Linearring appears to be different. It is exterior for the polygons and interior 
                 * for the holes. A good man should read both and discriminate*/

                if (myParser.Name.Equals("gml:PolygonPatch", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    currentPolygon = null;
                }

                if (myParser.Name.Equals("gml:PolygonPatch", StringComparison.OrdinalIgnoreCase) && (!myParser.IsStartElement()))
                {
                    polygons.Add(currentPolygon);
                }

                if (myParser.Name.Equals("gml:exterior", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    isPoly = 1;
                }

                if (myParser.Name.Equals("gml:interior", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    isPoly = 0;
                }

                /* if this is the list of points  - Bingo */
                if (myParser.Name.Equals("gml:posList", StringComparison.OrdinalIgnoreCase) && myParser.IsStartElement())
                {
                    Polygon poly = parsePts(layerId, myParser.ReadString(), ref numberOfEdges, ref totalEdgeLength);
                    /* Add this polygon to the list of polygons  to be returned if current state is Polygon
                     * else this is a hole and needs to be added to the current polygon whatever it is   */
                    if (isPoly == 1)
                    {
                        currentPolygon = poly;
                    }
                    else if (isPoly == 0)
                    {
                        Hole hole = new Hole(layerId, poly.getLines());
                        hole.parent = currentPolygon;
                        currentPolygon.addHole(hole);
                    }
                }
            }

            if (totalEdgeLength > 0)
            {
                avgLength = numberOfEdges / totalEdgeLength;
            }
            return new Layer(layerId, polygons, boundingBoxLower, boundingBoxUpper, avgLength);
        }

        private Polygon parsePts(int layer, string posList, ref double numberOfEdges, ref double totalEdgeLength)
        {
            /* Read the values of the points inside the polygon */
            char[] delimeters = { ' ', '\n', '\t' };
            String[] points = posList.Split(delimeters);
            Polygon poly = new Polygon(layer, null as List<Line>);
            List<Line> lines = new List<Line>();
            Point origin = null;

            for (int i = 0; i < points.Length - 2; i = i + 2)
            {

                /* Create objects for all the points - two points (4 values) of the line at a time 
                 * Make sure the last point is taken care of, as it doesn't have a next point */

                Point pt1 = new Point(double.Parse(points[i]), double.Parse(points[i + 1]));
                Point pt2 = new Point(double.Parse(points[i + 2]), double.Parse(points[i + 3]));

                /* Find length of this line */
                totalEdgeLength += Point.GetLineLength(pt1, pt2);
                numberOfEdges += 1;
                /* Create line connecting the points just read */
                Line ln = new Line(pt1, pt2);
                ln.setPolygon(poly);
                /* set layer id */
                ln.label = LayerID;

                /* If the current line is not the first line segment 
                 * set the previous of this line to the line 
                 * that was added to the list of lines last */

                if (i == 0)
                {
                    ln.setPrev(null);
                    /* Also if i is 0 the current point is the origin too */
                    origin = pt1;
                }
                else
                {
                    ln.setPrev(lines[lines.Count - 1]);
                    /* Similarly set the current line as next line of 
                     * the line that was added to the list last */
                    lines[lines.Count - 1].setNext(ln);
                }

                /* Add current line to the list */
                lines.Add(ln);

                /* origin is the last point too - right? Connect it to the last one */
                if (i == points.Length - 2)
                {

                    Line last = new Line(pt2, origin);
                    lines.Add(last);
                    /* Find length of this line */
                    totalEdgeLength += Point.GetLineLength(pt2, origin);
                    numberOfEdges += 1;
                }
            }

            /* Save this list of lines to the polygon */
            poly.lines = lines;
            return poly;
        }

        public Layer Read(Stream stream)
        {
            Layer layer = null;
            using (XmlTextReader reader = new XmlTextReader(stream))
            {
                layer = _parse(LayerID, reader);
            }
            return layer;
        }

        public Layer Read(string path)
        {
            Layer layer = null;
            using (XmlTextReader reader = new XmlTextReader(path))
            {
                layer = _parse(LayerID, reader);
            }
            return layer;
        }

        public void Write(Layer layer, Stream stream)
        {
            using (XmlTextWriter writer = new XmlTextWriter(stream, null))
            {
                _save(layer, writer);
            }
        }

        public void Write(Layer layer, string path)
        {
            using (XmlTextWriter writer = new XmlTextWriter(path, null))
            {
                _save(layer, writer);
            }
        }

        /* Find euclidean distance between two points */


        private static void _save(Layer layer, System.Xml.XmlTextWriter writer)
        {
            Point lower = layer.GetBoundingBox()[0];
            Point upper = layer.GetBoundingBox()[1];

            writer.WriteStartDocument();
            writer.Formatting = Formatting.Indented;
            /* Start the Feature collection element */
            writer.WriteStartElement("gml:FeatureCollection");
            writer.WriteAttributeString("xmlns:gml",  "http://www.opengis.net/gml");
            writer.WriteAttributeString("xmlns:xlink","http://www.w3.org/1999/xlink");
            writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
            writer.WriteAttributeString("xmlns:fme","http://www.safe.com/gml/fme");
            writer.WriteAttributeString("xsi:schemaLocation", "http://www.safe.com/gml/fme polygon1subset.xsd");

            writer.WriteStartElement("gml:boundedBy");
            writer.WriteStartElement("gml:Envelope");
            writer.WriteAttributeString("srsDimension", "2");
            writer.WriteStartElement("gml:lowerCorner");
            writer.WriteString(string.Format("{0} {1}", lower.X, lower.Y));
            writer.WriteEndElement(); //</gml:lowerCorner>
            writer.WriteStartElement("gml:upperCorner");
            writer.WriteString(string.Format("{0} {1}", upper.X, upper.Y));
            writer.WriteEndElement(); //</gml:upperCorner>
            writer.WriteEndElement(); //</gml:Envelope>
            writer.WriteEndElement(); //</gml:boundedBy>

            /* Write out each member */
            foreach (Polygon polygon in layer.GetPolygons())
            {
                writer.WriteStartElement("gml:featureMember");
                writer.WriteStartElement("fme:FID");
                writer.WriteEndElement();

                writer.WriteStartElement("gml:patches");
                writer.WriteStartElement("gml:PolygonPatch");
                writer.WriteStartElement("gml:exterior");
                writer.WriteStartElement("gml:LinearRing");
                writer.WriteStartElement("gml:posList");

                var pts = from pt in polygon.getPoints(true)
                          select string.Format("{0} {1}", pt.X, pt.Y);
                var t = polygon.getPoints();
                writer.WriteString(string.Join(" ", pts));
                writer.WriteEndElement(); //</gml:posList>
                writer.WriteEndElement(); //</gml:LinearRing>
                writer.WriteEndElement(); //</gml:exterior>

                List<Hole> holes = polygon.holes;
                if (holes.Count > 0)
                {
                    foreach (Hole h in holes)
                    {
                        writer.WriteStartElement("gml:interior");
                        writer.WriteStartElement("gml:LinearRing");
                        writer.WriteStartElement("gml:posList");
                        var hpts = from pt in h.getPoints()
                                   select string.Format("{0} {1}", pt.X, pt.Y);

                        writer.WriteString(string.Join(" ", hpts));
                        writer.WriteEndElement(); //</gml:posList>
                        writer.WriteEndElement(); //</gml:LinearRing>
                        writer.WriteEndElement(); //</gml:interior>
                    }
                }
                writer.WriteEndElement(); //<gml:PolygonPatch>
                writer.WriteEndElement(); //</gml:patches>
                writer.WriteEndElement(); //</gml:featureMember>
            }
            writer.WriteEndElement(); //</gml:FeatureCollection>
            writer.WriteEndDocument();
        }
    }
}
